package org.hjl.commonutils.idempotent.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.hjl.commonutils.idempotent.Idempotent;
import org.hjl.commonutils.idempotent.exception.IdempotentException;
import org.hjl.commonutils.idempotent.service.KeyResolver;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @author 公众号-吴农软语
 */
@Aspect
@Slf4j
public class IdempotentAspect {

    private static final ThreadLocal<Map<String, Object>> THREAD_CACHE = ThreadLocal.withInitial(HashMap::new);
    private static final String KEY = "key";

    private static final String DEL_KEY = "delKey";

    @Resource
    private Redisson redisson;

    @Resource
    private KeyResolver keyResolver;

    /**
     * Idempotent注解的包路径
     */
    @Pointcut("@annotation(com.XXX.XXX.Idempotent)")
    public void pointCut() {
    }

    @Before("pointCut()")
    public void beforePointCut(JoinPoint joinPoint) {
        //获取到当前请求的属性，进而得到HttpServletRequest对象，以便后续获取请求URL和参数信息。
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        //从JoinPoint中获取方法签名，并确认该方法是否被@Idempotent注解标记。如果是，则继续执行幂等性检查逻辑；如果不是，则直接返回，不进行幂等处理。。
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        if (!method.isAnnotationPresent(Idempotent.class)) {
            return;
        }
        Idempotent idempotent = method.getAnnotation(Idempotent.class);
        String key;
        // 若没有配置 幂等 标识编号，则使用 url + 参数列表作为区分;如果提供了key规则，则利用keyResolver根据提供的规则和切点信息生成键
        if (!StringUtils.hasLength(idempotent.key())) {
            String url = request.getRequestURL().toString();
            String argString = Arrays.asList(joinPoint.getArgs()).toString();
            key = url + argString;
        } else {
            // 使用jstl 规则区分
            key = keyResolver.resolver(idempotent, joinPoint);
        }
        //从注解中读取并设置幂等操作的过期时间、描述信息、时间单位以及是否删除键的标志。
        long expireTime = idempotent.expireTime();
        String info = idempotent.info();
        TimeUnit timeUnit = idempotent.timeUnit();
        boolean delKey = idempotent.delKey();
        //尝试从RMapCache（基于Redis的并发安全映射缓存）中获取键对应的值，如果存在，则说明重复操作，抛出IdempotentException
        RMapCache<String, Object> rMapCache = redisson.getMapCache(CommonConstants.RMAP_CACHE_KEY);
        String value = LocalDateTime.now().toString().replace("T", " ");
        Object v1;
        if (null != rMapCache.get(key)) {
            throw new IdempotentException(info);
        }
        //使用synchronized关键字保证多线程环境下操作的原子性，然后使用putIfAbsent方法尝试添加键值对到缓存中，若添加成功（即返回null），则记录日志并继续执行业务逻辑；若添加失败（即键已存在），同样抛出IdempotentException。
        synchronized (this) {
            v1 = rMapCache.putIfAbsent(key, value, expireTime, timeUnit);
            if (null != v1) {
                throw new IdempotentException(info);
            } else {
                log.info("[idempotent]:has stored key={},value={},expireTime={}{},now={}", key, value, expireTime,
                        timeUnit, LocalDateTime.now().toString());
            }
        }
        //将幂等键和是否删除键的标志存储到THREAD_CACHE（线程局部变量）中，供后续afterPointCut方法使用
        Map<String, Object> map = THREAD_CACHE.get();
        map.put(KEY, key);
        map.put(DEL_KEY, delKey);
    }

    @After("pointCut()")
    public void afterPointCut(JoinPoint joinPoint) {
        //尝试从THREAD_CACHE（线程局部变量）中获取之前存储的幂等相关信息，如果为空或者不存在，则直接返回，不做进一步处理
        Map<String, Object> map = THREAD_CACHE.get();
        if (CollectionUtils.isEmpty(map)) {
            return;
        }
        //检查RMapCache（基于Redis的并发安全映射缓存）是否为空，如果为空（即没有任何条目），表明无需执行任何清理动作，直接返回
        RMapCache<Object, Object> mapCache = redisson.getMapCache(CommonConstants.RMAP_CACHE_KEY);
        if (mapCache.size() == 0) {
            return;
        }
        //检查RMapCache（基于Redis的并发安全映射缓存）是否为空，如果为空（即没有任何条目），表明无需执行任何清理动作，直接返回
        String key = map.get(KEY).toString();
        boolean delKey = (boolean) map.get(DEL_KEY);

        if (delKey) {
            mapCache.fastRemove(key);
            log.info("[idempotent]:has removed key={}", key);
        }
        //无论是否移除了键，最后都会清空当前线程局部变量THREAD_CACHE中的数据，避免内存泄漏
        THREAD_CACHE.remove();
    }
}